<template>
	<div class="vue-scratchable-wrapper">
		<slot :init="init"></slot>
		<canvas
			@mousedown="mouseDown"
			@mousemove="mouseMove"
			@touchstart="touchDown"
			@touchmove="touchMove"
		/>
	</div>
</template>

<script>
import debounce from 'lodash/debounce';
export default {
	name: 'VueScratchable',
	props: {
		brushOptions: {
			type: Object,
			default: () => ({
				size: 20, // 涂层清除笔宽度
				shape: 'round', // 笔宽度两端样式
			}),
		},
		hideOptions: {
			type: Object,
			default: () => ({
				type: 'color', // pattern
				value: '#dadada', // 刮刮乐涂层颜色
				src: '', // 覆盖图片
				repeat: 'repeat', // 是否重复覆盖
			}),
		},
		getPercentageCleared: {
			type: Boolean,
			default: false, // 是否获取涂层清除百分比
		},
		percentageStride: {
			type: Number,
			default: 150, // 涂层清除百分比
		},
	},
	data() {
		return {
			canvas: null,
			context: null,
			isPressed: false,
			position: {
				currX: 0, // 鼠标相对涂层位置left
				currY: 0, // 鼠标相对涂层位置top
				lastX: 0,
				lastY: 0,
			},
			observer: null, // 观察者对象
			initFlag: false,
		};
	},
	mounted() {
		// this.$el 当前组件
		this.canvas = this.$el.querySelector('canvas');
		const debounceInit = debounce(() => this.init(), 200);
		// 观察者对象
		this.observer = new MutationObserver((mutations) => {
			mutations.forEach(({ attributeName }) => {
				if (this.initFlag || !attributeName) return;
				debounceInit();
			});
		});
		this.observer.observe(this.$el, {
			childList: true, // 子节点新增/删除
			attributes: true, // 属性节点
			characterData: true, // 文本节点,注释节点,以及处理指令节点
			subtree: true, // 所有后代节点,包含上述三种节点变化
		});
		window.addEventListener('mouseup', this.mouseUp, false);
		window.addEventListener('touchend', this.touchUp, false);
		window.addEventListener('touchcancel', this.touchUp, false);
		window.addEventListener('resize', debounce(this.init, 500));
		this.init();
	},
	destroyed() {
		window.removeEventListener('mouseup', this.mouseUp);
		window.removeEventListener('touchend', this.touchUp);
		window.removeEventListener('touchcancel', this.touchUp);
		window.removeEventListener('resize', debounce(this.init, 500));
		this.observer.disconnect(); // 暂停观察者对象节点的变化监听
	},
	methods: {
		init() {
			this.initFlag = true;
			this.setCanvasSizeAndContext();
			this.$nextTick(() => this.fillArea());
		},
		setCanvasSizeAndContext() {
			this.$nextTick(() => {
				const { width, height } = this.$el.getBoundingClientRect(); // 当前组件尺寸
				this.canvas.width = Math.ceil(width);
				this.canvas.height = Math.ceil(height);
				this.context = this.canvas.getContext('2d'); // 创建canvas画布
			});
		},
		async fillArea() {
			// canvas设置尺寸后，canvas绘制
			const { width, height } = this.context.canvas;
			await this.setFillStyle().catch((err) => {
				// eslint-disable-next-line no-console
				console.error(` 
					Failed to load image!
					Error: ${err.name}
					Message: ${err.message}
				`);
			});
			this.context.fillRect(0, 0, width, height); // canvas绘制
			this.initFlag = false;
		},
		setFillStyle() {
			const { type, value = '', src = '', repeat = '' } = this.hideOptions;
			this.context.globalCompositeOperation = 'source-over'; // 目标图像上显示源图像
			// eslint-disable-next-line no-return-assign
			if (type === 'color')
				return new Promise((resolve) => resolve((this.context.fillStyle = value)));
			return new Promise((resolve, reject) => {
				const img = new Image();
				img.onload = () => {
					this.context.fillStyle = this.context.createPattern(img, repeat); // 生成图片重复绘制canvas
					resolve(img);
				};
				img.onerror = (err) => reject(err);
				img.src = src;
			});
		},
		clearArea() {
			const { width, height } = this.context.canvas;
			this.context.globalCompositeOperation = 'destination-out';
			this.context.fillRect(0, 0, width, height);
		},
		draw() {
			const { currX, currY, lastX, lastY } = this.position;
			this.context.beginPath();
			this.context.globalCompositeOperation = 'destination-out';
			this.context.lineWidth = this.brushOptions.size;
			this.context.lineJoin = this.brushOptions.shape;
			this.context.moveTo(lastX, lastY);
			if (this.isPressed) {
				this.context.lineTo(currX, currY);
			} else {
				this.context.lineTo(currX + 0.01, currY + 0.01);
			}
			this.context.closePath();
			this.context.stroke();
			this.lastPositionHelper(currX, currY);
			this.calculateClearedArea();
		},
		lastPositionHelper(x, y) {
			this.position.lastX = x;
			this.position.lastY = y;
		},
		currentPositionHelper(x, y) {
			const { top, left } = this.canvas.getBoundingClientRect();
			this.position.currX = x - left;
			this.position.currY = y - top;
		},
		mouseDown({ clientX, clientY }) {
			// 鼠标相对于浏览器可视区域的 X,Y 坐标
			this.currentPositionHelper(clientX, clientY);
			this.lastPositionHelper(this.position.currX, this.position.currY);
			this.draw();
			this.isPressed = true;
		},
		mouseMove({ clientX, clientY }) {
			if (!this.isPressed) return;
			this.currentPositionHelper(clientX, clientY);
			this.draw();
		},
		mouseUp() {
			this.isPressed = false;
		},
		touchDown({ targetTouches: [{ clientX, clientY }] }) {
			this.currentPositionHelper(clientX, clientY);
			this.lastPositionHelper(this.position.currX, this.position.currY);
			this.draw();
			this.isPressed = true;
		},
		touchMove(event) {
			event.preventDefault();
			const {
				targetTouches: [{ clientX, clientY }],
			} = event;
			this.currentPositionHelper(clientX, clientY);
			this.draw();
		},
		touchUp() {
			this.isPressed = false;
		},
		calculateClearedArea() {
			if (!this.getPercentageCleared) return;
			const { width, height } = this.context.canvas;
			const {
				data,
				data: { length },
			} = this.context.getImageData(0, 0, width, height);
			const total = length / this.percentageStride;
			let clearedCount = 0;
			for (let i = clearedCount; i < length; i += this.percentageStride) {
				if (parseInt(data[i], 10) === 0) clearedCount += 1;
			}
			this.$emit('percentage-update', Math.round((clearedCount / total) * 100));
		},
	},
};
</script>

<style scoped>
.vue-scratchable-wrapper {
	position: relative;
}
.vue-scratchable-wrapper > * {
	user-select: none;
}
canvas {
	position: absolute;
	top: 0;
	left: 0;
}
</style>